package com.fourpigeon.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.mail.Mail;
import cn.hutool.json.JSONUtil;
import com.fourpigeon.dao.*;
import com.fourpigeon.domain.*;
import com.fourpigeon.service.BlogService;
import com.fourpigeon.utils.IdGenerator;
import com.fourpigeon.utils.MailUtil;
import com.fourpigeon.utils.UserHolder;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiFunction;
import java.util.function.Function;

import static com.fourpigeon.utils.IdGenerator.BEGIN_TIMESTAMP;
import static com.fourpigeon.utils.IdGenerator.BIT_COUNT;
import static com.fourpigeon.utils.RedisConstants.*;
//
@Slf4j
@Service("blogService")
public class BlogServiceImpl implements BlogService{
    @Autowired
    private BlogDailyDao blogDailyDao;
    @Autowired
    private BlogCommentDao blogCommentDao;
    @Autowired
    private UserDao userDao;
    @Autowired
    private BlogDealDao blogDealDao;
    @Autowired
    private BlogAgreeDao blogAgreeDao;
    @Autowired
    private BlogPracticeDao blogPracticeDao;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private IdGenerator idGenerator;
    @Autowired
    private AgreeInfoDao agreeInfoDao;
    @Autowired
    private MailUtil mailUtil;

    //约定贴获取Map(同时解决缓存击穿)
    public Map<Short, Function<Long,BlogAgree>> agreeGetMap = new HashMap<>();
    {
        agreeGetMap.put((short) 0,(blogid) -> searchBlog(BLOG_TTL_KEY+"junior:",blogid,BlogAgree.class,(id)-> blogAgreeDao.getByJuniorId(id)));
        agreeGetMap.put((short) 1,(blogid) -> searchBlog(BLOG_TTL_KEY+"junior:",blogid,BlogAgree.class,(id)-> blogAgreeDao.getByAdvancedId(id)));
        agreeGetMap.put((short) 2,(blogid) -> searchBlog(BLOG_TTL_KEY+"junior:",blogid,BlogAgree.class,(id)-> blogAgreeDao.getBySeniorId(id)));
    }

    //约定贴更新Map
    public Map<Short, Function<BlogAgree,Boolean>> agreeUpdateMap = new HashMap<>();
    {
        agreeUpdateMap.put((short) 0,(b) -> blogAgreeDao.updateJunior(b));
        agreeUpdateMap.put((short) 1,(b) -> blogAgreeDao.updateAdvanced(b));
        agreeUpdateMap.put((short) 2,(b) -> blogAgreeDao.updateSenior(b));
    }

    //约定贴发布Map
    public Map<Short, Function<BlogAgree,Boolean>> agreeSaveMap = new HashMap<>();
    {
        agreeSaveMap.put((short) 0,(b) -> blogAgreeDao.saveJunior(b));
        agreeSaveMap.put((short) 1,(b) -> blogAgreeDao.saveAdvanced(b));
        agreeSaveMap.put((short) 2,(b) -> blogAgreeDao.saveSenior(b));
    }

    //约定贴分页查询Map
    public Map<Short, Function<Long,List<BlogAgree>>> agreeGetsMap = new HashMap<>();
    {
        agreeGetsMap.put((short) 0,(b) -> blogAgreeDao.getJuniors(b));
        agreeGetsMap.put((short) 1,(b) -> blogAgreeDao.getAdvanceds(b));
        agreeGetsMap.put((short) 2,(b) -> blogAgreeDao.getSeniors(b));
    }

    //约定贴分页查询当前用户Map
    public Map<Short, BiFunction<Long,Long,List<BlogAgree>>> userAgreeGetsMap = new HashMap<>();
    {
        userAgreeGetsMap.put((short) 0,(userId,startId) -> blogAgreeDao.getJuniorByUserId(userId,startId));
        userAgreeGetsMap.put((short) 1,(userId,startId) -> blogAgreeDao.getAdvancedByUserId(userId,startId));
        userAgreeGetsMap.put((short) 2,(userId,startId) -> blogAgreeDao.getSeniorByUserId(userId,startId));
    }

    //用户搜索约定贴分页查询Map
    public Map<Short, BiFunction<String, Short, List<BlogAgree>>> searchAgreeGetsMap = new HashMap<>();
    {
        searchAgreeGetsMap.put((short) 0,(searchKeyWord, type) -> blogAgreeDao.getSearchJuniors(searchKeyWord, type));
        searchAgreeGetsMap.put((short) 1,(searchKeyWord, type) -> blogAgreeDao.getSearchAdvances(searchKeyWord, type));
        searchAgreeGetsMap.put((short) 2,(searchKeyWord, type) -> blogAgreeDao.getSearchSeniors(searchKeyWord, type));
    }

    //用户搜索全部约定贴分页查询Map
    public Map<Short, Function<String, List<BlogAgree>>> searchAllAgreeGetsMap = new HashMap<>();
    {
        searchAllAgreeGetsMap.put((short) 0, (searchKeyWord) ->blogAgreeDao.getAllSearchJuniors(searchKeyWord));
        searchAllAgreeGetsMap.put((short) 1, (searchKeyWord) ->blogAgreeDao.getAllSearchAdvances(searchKeyWord));
        searchAllAgreeGetsMap.put((short) 2, (searchKeyWord) ->blogAgreeDao.getAllSearchSeniors(searchKeyWord));

    }

    //约定贴分页查询Map
    public Map<Short, Function<Short,List<BlogAgree>>> typeAgreeGetsMap = new HashMap<>();
    {
        typeAgreeGetsMap.put((short) 0,(b) -> blogAgreeDao.getJuniorsByType(b));
        typeAgreeGetsMap.put((short) 1,(b) -> blogAgreeDao.getAdvancesByType(b));
        typeAgreeGetsMap.put((short) 2,(b) -> blogAgreeDao.getSeniorsByType(b));
    }

    //代理对象
    private BlogService proxy;

    @Deprecated
    /**
     * 加入约球贴接口实现类
     * @param blogId 博客id
     * @return 结果信息
     */
    @Override
    public Result joinAgreeBlog(Long blogId) {
        Long userId = UserHolder.getUser().getId();
        //执行lua脚本(Redis中-1并且加入用户到预约信息中)
        Long result = stringRedisTemplate.execute(
                JOIN_SCRIPT,
                Collections.emptyList(),
                blogId.toString(),
                userId.toString()
        );
        int rs = result.intValue();
        //判断脚本执行结果
        if (rs != 0){
            return Result.fail(rs == 1 ? "约球人数已满!" : "请勿重复加入球局!");
        }
        //设置预约信息
        AgreeInfo agreeInfo = new AgreeInfo();
        agreeInfo.setId(1L);
        agreeInfo.setBlogId(blogId);
        agreeInfo.setUserId(userId);
        //加入阻塞队列
        agreeTasks.add(agreeInfo);
        //获取代理对象
        proxy = (BlogService) AopContext.currentProxy();
        //返回结果
        return Result.ok("预定成功!");
    }

    @Deprecated
    //阻塞队列
    private final BlockingQueue<AgreeInfo> agreeTasks = new ArrayBlockingQueue<>(1024*1024);
    @Deprecated
    //操作阻塞队列的线程池
    private static final ExecutorService AGREE_EXECUTOR = Executors.newSingleThreadExecutor();
    @Deprecated
    //为线程提供需要执行的操作
    @PostConstruct
    private void init(){
        AGREE_EXECUTOR.submit(new AgreeHandler());
    }

    @Deprecated
    /**
     * 操作阻塞队列的线程的执行逻辑
     */
    private class AgreeHandler implements Runnable{
        @Override
        public void run() {
            while(true){
                try{
                    //获取阻塞队列中的信息
                    AgreeInfo agreeInfo = agreeTasks.take();
                    //创建预约信息
                    handlerAgreeOrder(agreeInfo);
                }catch (Exception e){
                    log.error("处理预约信息异常",e);
                }
            }
        }
    }
    @Deprecated
    /**
     * 操作数据库创建预约信息
     * @param agreeInfo 预约信息
     */
    private void handlerAgreeOrder(AgreeInfo agreeInfo) {
        //TODO 获取用户id
        Long userId = agreeInfo.getUserId();
        //创建Redisson的锁对象
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //尝试获取分布式锁，失败则直接返回
        boolean isLock = lock.tryLock();
        //判断锁是否获取成功
        if(!isLock){
            //获取失败
            log.error("不允许重复加入同一个球局!");
            return;
        }
        try{
            proxy.createAgreeInfo(agreeInfo);
        }finally {
            //释放锁
            lock.unlock();
        }
    }
    @Deprecated
    @Override
    @Transactional //开启事务
    /**
     * 预约信息入库
     * @param agreeInfo 约球信息对象
     */
    public void createAgreeInfo(AgreeInfo agreeInfo) {
        Long blogId = agreeInfo.getBlogId();
        //查询球局剩余人数
        BlogAgree blogAgree = blogAgreeDao.getByJuniorId(blogId);
        if (blogAgree.getCurpeople().equals(blogAgree.getPeople())) {
            log.error("预约已满!");
            return;
        }
        //未满，+1
        Short curpeople = blogAgree.getCurpeople();
        curpeople++;
        blogAgree.setCurpeople(curpeople);
        //更新预约信息
        boolean isSuccess = blogAgreeDao.updateJunior(blogAgree);
        if (!isSuccess) {
            log.error("预约已满!");
            return;
        }
        agreeInfo.set();
        //预约信息写入数据库
        agreeInfoDao.save(agreeInfo);
    }

    /**
     * 点赞/取消点赞日常贴
     * @param blogId 博客id
     * @return 结果信息
     */
    @Override
    public Result likeBlogOrNot(Long blogId) {
        Long userId = UserHolder.getUser().getId();
        Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKE_KEY + blogId, userId.toString());
        if (score == null) {
            //未点赞
            if (blogDailyDao.addLike(blogId)) {
                stringRedisTemplate.opsForZSet().add(BLOG_LIKE_KEY+blogId,userId.toString(),System.currentTimeMillis());
                return Result.ok("like");
            }
        }
        else{
            //已点赞
            if (blogDailyDao.subLike(blogId)) {
                stringRedisTemplate.opsForZSet().remove(BLOG_LIKE_KEY+blogId,userId.toString());
                return Result.ok("cancel");
            }
        }
        return Result.ok();
    }

    /**
     * 点赞/取消点赞评论
     * @param commentId 评论id
     * @return 结果信息
     */
    @Override
    public Result likeComment(Long commentId) {
        Long userId = UserHolder.getUser().getId();
        Double score = stringRedisTemplate.opsForZSet().score(COMMENT_LIKE_KEY + commentId, userId.toString());
        if (score == null) {
            //未点赞
            if (blogCommentDao.addLike(commentId)) {
                stringRedisTemplate.opsForZSet().add(COMMENT_LIKE_KEY+commentId,userId.toString(),System.currentTimeMillis());
            }
        }
        else{
            //已点赞
            if (blogCommentDao.subLike(commentId)) {
                stringRedisTemplate.opsForZSet().remove(COMMENT_LIKE_KEY+commentId,userId.toString());
            }
        }
        return Result.ok();
    }

    /**
     * 点击量Redis键后缀
     */
    public Map<Integer,String> clickMap = new HashMap<>();
    {
        clickMap.put(0,"daily:");
        clickMap.put(1,"deal:");
        clickMap.put(2,"practice");
    }

    /**
     * 获取点击量
     * @param blogType 0:日常，1:交易，2:陪教
     * @param blogId 博客id
     * @return 空
     */
    @Override
    public Result clickCount(Integer blogType,Long blogId) {
        //HyperLogLog实现方法
//        stringRedisTemplate.opsForHyperLogLog().add(BLOG_CLICK_KEY+blogId, UserHolder.getUser().getId().toString());
        //单纯点击量实现方法
//        if(stringRedisTemplate.opsForValue().get(BLOG_CLICK_KEY+blogId)==null){
//            stringRedisTemplate.opsForValue().set(BLOG_CLICK_KEY+blogId,"0");
//        }
//        stringRedisTemplate.opsForValue().increment(BLOG_CLICK_KEY+blogId);
        //获取当前贴子点击量
        Double score = stringRedisTemplate.opsForZSet().score(BLOG_CLICK_KEY+clickMap.get(blogType), blogId.toString());
        if (score == null) {
            //不存在
            stringRedisTemplate.opsForZSet().add(BLOG_CLICK_KEY+clickMap.get(blogType),blogId.toString(),1);
        }else {
            //存在，自增
            stringRedisTemplate.opsForZSet().incrementScore(BLOG_CLICK_KEY+clickMap.get(blogType),blogId.toString(),1);
        }
        return Result.ok();
    }

    /**
     * 举报贴子
     * @param blogType 贴子种类：0:日常贴、1:交易贴、20:junior贴、21:advanced贴、22:senior贴、3:陪教贴
     * @param blogId 贴子id
     * @return 结果
     */
    @Override
    public Result reportBlog(Short blogType,Long blogId,String content) {
        String prefix = "举报帖子:";
        switch (blogType) {
            case 0 -> {
                blogDailyDao.reportBlog(blogId);
                mailUtil.sendSimpleMail(prefix + "日常:" + blogId, content);
            }
            case 1 -> {
                blogDealDao.reportBlog(blogId);
                mailUtil.sendSimpleMail(prefix + "交易:" + blogId, content);
            }
            case 20 -> {
                blogAgreeDao.reportBlogJ(blogId);
                mailUtil.sendSimpleMail(prefix + "junior:" + blogId, content);
            }
            case 21 -> {
                blogAgreeDao.reportBlogA(blogId);
                mailUtil.sendSimpleMail(prefix + "advanced:" + blogId, content);
            }
            case 22 -> {
                blogAgreeDao.reportBlogS(blogId);
                mailUtil.sendSimpleMail(prefix + "senior:" + blogId, content);
            }
            case 3 -> {
                blogPracticeDao.reportBlog(blogId);
                mailUtil.sendSimpleMail(prefix + "陪教:" + blogId, content);
            }
            default -> {
            }
        }
        return Result.ok("举报成功!");
    }

    /**
     * 判断日常贴是否被当前用户点赞
     * @param blogId 博客id
     * @return 结果信息
     */
    public Boolean likeOrNot(Long blogId) {
        if(UserHolder.getUser()==null) return false;
        Long userId = UserHolder.getUser().getId();
        Double score = stringRedisTemplate.opsForZSet().score(BLOG_LIKE_KEY + blogId, userId.toString());
        return score != null;
    }

    /**
     * 判断评论是否被当前用户点赞
     * @param commentId 评论id
     * @return 结果信息
     */
    public Boolean likeOrNotCom(Long commentId) {
        if(UserHolder.getUser()==null) return false;
        Long userId = UserHolder.getUser().getId();
        Double score = stringRedisTemplate.opsForZSet().score(COMMENT_LIKE_KEY + commentId, userId.toString());
        return score != null;
    }

    /**
     * 检验用户
     * @return 用户是否存在
     */
    public boolean checkUser(){
        Long userId = UserHolder.getUser().getId();
        User user = userDao.getById(userId);
        return user != null;
    }

    /**
     * 泛型处理各类发布方法(约球贴另外处理)
     * @param dto 接收前端的DTO对象
     * @param tarClass 加入数据库时需要转换成的类
     * @param values 日常贴的博客类型
     * @return Result
     * @param <DTO> DTO对象泛型
     * @param <TAR> 目标类泛型
     */
    public <DTO,TAR> Result publish(DTO dto,Class<TAR> tarClass,short...values){
        Long userId = UserHolder.getUser().getId();
        //加入分布式锁(解决单个用户发帖时发生线程安全问题)
        RLock lock = redissonClient.getLock(BLOG_PUBLISH_LOCK + userId);
        //尝试获取锁，无等待时间，无限释放时间(含看门狗机制)
        boolean isSuccess = lock.tryLock();
        //获取锁失败
        if (!isSuccess) {
            return Result.fail("发帖太频繁了哦~请稍后再试!");
        }
        //获取成功，获取用户信息
        User user = userDao.getById(userId);
        String ttl = userId.toString();
        //检验Redis的key是否过期
        if (stringRedisTemplate.opsForValue().get(BLOG_CREATE_STRING_TTL+ttl) != null) {
            return Result.fail("发帖太频繁了哦~请稍后再试!");
        }
        //创建对象
        TAR tar = BeanUtil.copyProperties(dto, tarClass);
        //生成blogId
        Long blogId = idGenerator.nextBlogId("blog");
        //保存
        if (dto.getClass()==BlogDealDTO.class) {
            ((BlogDeal) tar).set(blogId,user);
            blogDealDao.save((BlogDeal) tar);
            String imagesStr = ((BlogDeal) tar).getImages();
            //图片保存Redis记录
            List<String> list = StrUtil.split(imagesStr, ',');
            for (String s : list) {
                //删除暂存记录
                stringRedisTemplate.delete(UPLOAD_FILE_CONFIRM_STRING+s);
                //确认保存，排除垃圾处理
                stringRedisTemplate.opsForSet().add(UPLOAD_FILE_CONFIRM_SET,s);
            }
        } else if (dto.getClass()== BlogDailyDTO.class) {
            ((BlogDaily) tar).set(blogId,user,values[0]);
            String imagesStr = ((BlogDaily) tar).getImages();
            //图片保存Redis记录
            List<String> list = StrUtil.split(imagesStr, ',');
            for (String s : list) {
                //删除暂存记录
                stringRedisTemplate.delete(UPLOAD_FILE_CONFIRM_STRING+s);
                //确认保存，排除垃圾处理
                stringRedisTemplate.opsForSet().add(UPLOAD_FILE_CONFIRM_SET,s);
            }
            blogDailyDao.save((BlogDaily)tar);
            //评论数量加入Redis
            stringRedisTemplate.opsForValue().set(BLOG_COMMENT_NUM+blogId,"0");
        } else if (dto.getClass()==BlogPracticeDTO.class) {
            ((BlogPractice) tar).set(blogId,user);
            blogPracticeDao.save((BlogPractice)tar);
        } else if (dto.getClass()== BlogAgreeDTO.class){
            ((BlogAgree) tar).set(blogId, user);
            agreeSaveMap.get(values[0]).apply((BlogAgree) tar);
        } else {
            //释放锁
            lock.unlock();
            return Result.fail("类型错误!请联系工作人员");
        }
        //创建RedisTTL，设置短时TTL，避免同一个用户发送过多请求同时创建贴子导致数据库压力过大
        //使用String类型存储，3sTTL
        stringRedisTemplate.opsForValue().set(BLOG_CREATE_STRING_TTL+ttl,ttl,3,TimeUnit.SECONDS);
        //释放分布式锁
        lock.unlock();
        //返回结果信息
        return Result.ok("发布成功");
    }

    /**
     * 搜索博客信息
     * @param keyPrefix RedisKey前缀
     * @param blogId 博客id
     * @param tarType 博客类
     * @param searchSQL 搜索的SQL语句
     * @return 博客对象
     * @param <C> 博客类泛型
     */
    public <C> C searchBlog(String keyPrefix, Long blogId,Class<C> tarType, Function<Long,C> searchSQL){
        String key = keyPrefix+blogId;
        String result = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isNotBlank(result)){
            //Redis命中，按类型转换成对象返回
            return JSONUtil.toBean(result,tarType);
        }
        //未命中，判断是否为之前返回给请求的空值
        if (result != null) {
            return null;
        }
        C c = searchSQL.apply(blogId);
        if (c == null) {
            //可能缓存击穿，Redis设置一分钟TTL的空值
            stringRedisTemplate.opsForValue().set(key,"",1,TimeUnit.MINUTES);
            return null;
        }
        //存在，对象转换成JSON写入Redis，设置15minTTL，返回对象
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(c),15,TimeUnit.MINUTES);
        return c;
    }

    /**
     * 处理分页查询中起始id为0的问题
     * @param startId 起始id
     * @return 处理后的startId变量
     */
    public long zeroHandler(Long startId) {
        if (startId != 0) return startId;
        startId = ((Long) (LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - BEGIN_TIMESTAMP)).intValue() + 10086L;;
        startId = startId << BIT_COUNT;
        System.out.println("startId: "+startId);
        return startId;
    }
}
